/*
 * Copyright 2012 Finbudget.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package finbudgettable.client.ui.grid.cell.input;

import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.*;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.user.client.*;
import com.google.gwt.user.client.ui.impl.TextBoxImpl;
import finbudgettable.client.ui.grid.cell.EditableGridCell;

/**
 * An editable text cell. Click to edit, escape to cancel, return to commit.
 *
 * @see com.google.gwt.cell.client.EditTextCell
 */
public abstract class InputEditableCell<V> extends EditableGridCell<V, String> {

    interface Resources extends ClientBundle {

        interface Style extends CssResource {

            @ClassName("b-editor-with-error")
            String bEditorWithError();

            @ClassName("b-editor-with-error__input")
            String bEditorWithError__input();

            @ClassName("b-input-cell__input")
            String bInputCell__input();
        }

        @Source("InputCell.css")
        Style style();
    }

    interface Template extends SafeHtmlTemplates {

        @Template("<input class=\"{0}\" type=\"text\" value=\"{1}\" tabindex=\"-1\" />")
        SafeHtml input(String className, String value);

        @Template("<div class=\"{0}\">{1}</div>")
        SafeHtml value(String className, String value);

        @Template("<div class=\"{0}\">&nbsp;</div>")
        SafeHtml nullValue(String className);
    }

    private static final Template template = GWT.create(Template.class);
    private static final Strings strings = GWT.create(Strings.class);
    static final Resources RESOURCES = GWT.create(Resources.class);

    static {
        RESOURCES.style().ensureInjected();
    }

    public InputEditableCell(boolean nullable) {
        super(nullable);
    }


    @Override
    public void onBrowserEvent(Context context, Element parent, V value,
                               NativeEvent event, ValueUpdater<V> valueUpdater) {
        if (isReadonly()) {
            return;
        }
        Object key = context.getKey();
        ViewData viewData = getViewData(key);
        String type = event.getType();
        boolean displaying = viewData == null || !viewData.isEditing();
        if (displaying) {
            int keyCode = event.getKeyCode();
            boolean keyPressedButNotEnter = BrowserEvents.KEYPRESS.equals(type) && keyCode != KeyCodes.KEY_ENTER;
            int charCode = event.getCharCode();
            if (keyPressedButNotEnter && charCode > 0) {
                String newValue = getStringFromCharCode(charCode);
                if (isValidChar(newValue)) {
                    if (viewData == null) {
                        viewData = new ViewData(value, convertValueToEditedValue(value));
                        setViewData(key, viewData);
                    }
                    viewData.setEditing(true);
                    viewData.setCurrentValue(newValue);
                    startEdit(context, parent, value, viewData);
                    event.preventDefault();
                    return;
                }
            }
        }
        super.onBrowserEvent(context, parent, value, event, valueUpdater);
    }

    @Override
    protected SafeHtml getEditorHtml(ViewData viewData) {
        String className = RESOURCES.style().bInputCell__input();
        if (!viewData.isValidated()) {
            className += " " + RESOURCES.style().bEditorWithError__input();
        }
        return template.input(className, viewData.getCurrentValue());
    }

    protected abstract String getValueToDisplay(V value);

    @Override
    protected SafeHtml getViewHtml(V value) {
        String displayValue = getValueToDisplay(value);
        if (displayValue != null && !displayValue.isEmpty()) {
            return template.value(getInnerCellClassName(), displayValue);
        } else {
            return template.nullValue(getInnerCellClassName());
        }
    }

    protected abstract String getInnerCellClassName();

    @Override
    protected SafeHtml getErrorHtml(String value) {
        String errorClassName = RESOURCES.style().bEditorWithError__input();
        if (value != null && !value.isEmpty()) {
            return template.value(errorClassName, value);
        } else {
            return template.nullValue(errorClassName);
        }
    }

    @Override
    protected void setFocus(Element parent, boolean focused) {
        InputElement input = getInputElement(parent);
        input.focus();
    }

    @Override
    protected String getEditedValue(Element parent) {
        InputElement input = getInputElement(parent);
        String value = input.getValue();
//        GWT.log(value);
        return value;
    }

    @Override
    protected void showIncorrectValueError(final Context context, final Element parent, final ViewData viewData) {
        Window.alert(strings.incorrectValue(viewData.getCurrentValue()));
        /*Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
            @Override
            public void execute() {
                startEdit(context, parent, viewData.getStoredValue(), viewData);
            }
        });*/
    }

    /**
     * Clear selected from the input element. Both Firefox and IE fire spurious
     * onblur events after the input is removed from the DOM if selection is not
     * cleared.
     *
     * @param input the input element
     */
    private native void clearInput(Element input) /*-{
        if (input.selectionEnd)
            input.selectionEnd = input.selectionStart;
        else if ($doc.selection)
            $doc.selection.clear();
    }-*/;

    /**
     * Get the input element in edit mode.
     */
    private InputElement getInputElement(Element parent) {
        return parent.getFirstChildElement().cast();
    }

    @Override
    protected void cancel(Context context, Element parent, V value, ViewData viewData) {
        clearInput(getInputElement(parent));
        super.cancel(context, parent, value, viewData);
    }

    @Override
    protected void commit(Context context, Element parent, ViewData viewData,
                          ValueUpdater<V> valueUpdater) {
        InputElement input = getInputElement(parent);
        clearInput(input);
        super.commit(context, parent, viewData, valueUpdater);
    }

    private static final TextBoxImpl textBoxImpl = GWT.create(TextBoxImpl.class);

    private void repeatEvent(final Element parent, final int keyCode) {
        Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
            @Override
            public void execute() {
                parent.dispatchEvent(Document.get().createKeyDownEvent(false, false, false, false, keyCode));
            }
        });
    }

    @Override
    protected void editEvent(Context context, Element parent, ViewData viewData, NativeEvent event,
                             ValueUpdater<V> valueUpdater) {
        String type = event.getType();
        boolean keyUp = BrowserEvents.KEYUP.equals(type);
        boolean keyDown = BrowserEvents.KEYDOWN.equals(type);
        if (keyUp || keyDown) {
            int keyCode = event.getKeyCode();
            if (keyDown && (keyCode == KeyCodes.KEY_DOWN || keyCode == KeyCodes.KEY_UP)) {
                commit(context, parent, viewData, valueUpdater);
                if (viewData.isValidated()) {
                    repeatEvent(parent, keyCode);
                } else {
                    showIncorrectValueError(context, parent, viewData);
                }
                event.preventDefault();
                return;

            } else if (keyDown && (keyCode == KeyCodes.KEY_LEFT || keyCode == KeyCodes.KEY_HOME)) {
                int cursorPos = textBoxImpl.getCursorPos(parent.getFirstChildElement());
                if (cursorPos == 0) {
                    commit(context, parent, viewData, valueUpdater);
                    if (viewData.isValidated()) {
                        repeatEvent(parent, keyCode);
                    } else {
                        showIncorrectValueError(context, parent, viewData);
                        event.preventDefault();
                    }
                }
                return;

            } else if (keyDown && (keyCode == KeyCodes.KEY_RIGHT || keyCode == KeyCodes.KEY_END)) {
                InputElement input = (InputElement) parent.getFirstChildElement();
                int cursorPos = textBoxImpl.getCursorPos(parent.getFirstChildElement());
                if (cursorPos == input.getValue().length()) {
                    commit(context, parent, viewData, valueUpdater);
                    if (viewData.isValidated()) {
                        event.preventDefault();
                        repeatEvent(parent, keyCode);
                    } else {
                        showIncorrectValueError(context, parent, viewData);
                        event.preventDefault();
                    }
                }
                return;
            }
        } else if (BrowserEvents.BLUR.equals(type)) {
            // Commit the change. Ensure that we are blurring the input element and
            // not the parent element itself.
            EventTarget eventTarget = event.getEventTarget();
            if (Element.is(eventTarget)) {
                Element target = Element.as(eventTarget);
                if ("input".equals(target.getTagName().toLowerCase())) {
                    commit(context, parent, viewData, valueUpdater);
                    if (!viewData.isValidated()) {
                        showIncorrectValueError(context, parent, viewData);
                    }
                }
            }
            return;
        }

        super.editEvent(context, parent, viewData, event, valueUpdater);

        InputElement input = getInputElement(parent);
        if (input != null) {
	        if (viewData.isValidated()) {
	            input.removeClassName(RESOURCES.style().bEditorWithError__input());
	        } else {
	            input.addClassName(RESOURCES.style().bEditorWithError__input());
	        }
        }
    }

    @Override
    protected void edit(Context context, Element parent, V value) {
        super.edit(context, parent, value);
        InputElement input = getInputElement(parent);
        TableCellElement td = parent.getParentElement().cast();
        int height = td.getAbsoluteBottom() - td.getAbsoluteTop() - 4;
        input.getStyle().setHeight(height, Style.Unit.PX);
        setCursorToTheEnd(input);
    }

    private void setCursorToTheEnd(InputElement input) {
        textBoxImpl.setSelectionRange((Element) input.cast(), input.getValue().length(), 0);
    }

}